package Algorithms;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import piis.SimulatedAnnealingSolution;
import piis.TestData;

/**
 *
 * @author Atuan
 */
public class SimulatedAnnealing {
    // Value of Temperature
    private double startingTemp = 10;
    // Value of CoolingRate, which is used to change the temperature.
    private double coolingRate = 0.99;
    public List<String> printResult = new ArrayList<>();
    
    public SimulatedAnnealing(){
        
    }
    
    public SimulatedAnnealing(String var, double interval){
        if(!var.equals("task6")){
            startingTemp = generateInitialTemperature(var,1);
            System.out.println("TEMPE: "+startingTemp);
        }
    }
    
    public double generateInitialTemperature(String var, double interval) {
        double temperature = 0.0;
        double fV;
        double sV;
        for (int i = 0; i < 100; i++) {
            fV = Math.random() * interval;
            sV = fV + Math.random() / 100;
            temperature += (-Math.abs(func(sV) - func(fV)) / Math.log(0.8));
        }
        return temperature / 10;
    }
    
    /**
     * This method finds the global minimum
     * @return : object which holds the value of the minimum and its x-coordiate.
     */
    public SimulatedAnnealingSolution findMin(long iterations,double min, double max){
        double result, result2, prob, r;
//         Select random value from 0 to 1 as first index.
        double random = (min+max)/2.0;
//        double random = Math.random();
        double prevIndex = random;
        double bestX = random;
        result = func(prevIndex);
        // Main loop
        for(int i = 0; i < iterations; i++){   
            prevIndex = findClosePoint(prevIndex,min,max);
            result2 = func(prevIndex);
            // If the new result is better, let it be the new bestResult.
            if(result2 < result){
                result = result2;
                bestX = prevIndex;
            } else {
                // If the new result is worse, count the probablity, choose another random number from the set [0,1].
                prob = Math.pow(Math.E,-(Math.abs(result2-result)/startingTemp));
                r = Math.random();
                //If the probablity is bigger than the random number, let the new result be the new best solution.
                if(r <= prob){
                    result = result2;
                    bestX = prevIndex;
                }
                countNewTemp(i);
            }
            if(i % 100 == 0)
            printResult.add(startingTemp+" "+ bestX+" "+result);
        }
        
        return new SimulatedAnnealingSolution(bestX,result);
    }
    
    public double[] findSolution(int iteration, double[] params, List<TestData> data, double min, double max){
        double prob, r;
        double[] result2;        
        result2 = Arrays.copyOf(params, params.length);
        
        for(int j = 0; j < params.length; j++){
            result2[j] = findClosePoint(params[j],500);
            
            // If the new result is better, let it be the new bestResult.
            double newError = getError(result2,data);
            double oldError = getError(params,data);
            if(iteration % 10000 == 0)System.out.println(iteration+" Errors: "+newError+" __ "+oldError);
            if(newError < oldError){
                params[j] = result2[j];
            } else {
                // If the new result is worse, count the probablity, choose another random number from the set [0,1].
                prob = Math.pow(Math.E,-((newError - oldError)/startingTemp));
                r = Math.random();                
                //If the probablity is bigger than the random number, let the new result be the new best solution.
                if(r <= prob){
                    params[j] = result2[j];
                } else {
                    result2[j] = params[j];
                }
            }
        }
        countNewTemp(iteration);
        if(iteration % 10000 == 0){
//            System.out.println("RESULT2 "+iteration+" "+getError(params,data));
        }
        
        return params;
    }
    
    public double[] findLRSolution(int iteration, double[] params, double[][] x, double y[], double min, double max){
        double prob, r;
        double[] result2;        
        result2 = Arrays.copyOf(params, params.length);
        
        for(int j = 0; j < params.length; j++){
            result2[j] = findClosePoint(params[j],500);
            
            // If the new result is better, let it be the new bestResult.
            double newError = getError(result2,x,y);
            double oldError = getError(params,x,y);
            if(newError < oldError){
                params[j] = result2[j];
            } else {
                // If the new result is worse, count the probablity, choose another random number from the set [0,1].
                prob = Math.pow(Math.E,-((newError - oldError)/startingTemp));
                r = Math.random();                
                //If the probablity is bigger than the random number, let the new result be the new best solution.
                if(r <= prob){
                    params[j] = result2[j];
                } else {
                    result2[j] = params[j];
                }
            }
        }
        countNewTemp(iteration);
        if(iteration % 10000 == 0){
            System.out.println("RESULT2 "+iteration+" "+getError(params,x,y));
        }
        return params;
    }
    
    public double getFunction(int i, double[] params, double[][] x){
        double f = params[0];
	for(int j = 1; j<params.length;j++){
            f+= params[j]*x[j-1][i];
	}
	return f;
    }
    
    public double getError(double[] result, double[][] x, double[] y){
        double error = 0;
        for(int i = 0; i<y.length; i++){
            error += Math.pow((getFunction(i,result,x) - y[i]),2);
//                    error += ;
        }
        error = error / (2*y.length);
        return error;
    }
    
    public double getError(double[] result, List<TestData> data){
        double error = 0;
        double constValue = 0.00001;
        for(TestData d : data){
            double range = d.getMax()-d.getMin();
            double priority = 4;
            double rand = Math.random();
            switch(d.getAction()){
                case "TRAIN":{
                    double err=0.0;
                    for(int i = 0; i<d.getY().size(); i++){
                        err += Math.pow((getFunction(i,result,d) - d.getY().get(i)),2);
                    }
                    error += (err / (2*d.getY().size()));
                } break;
                case "MIN":{
                    if(checkExt(result,d)){
                        error += rand*constValue*range*(priority/2);
                    }
                } break;
                case "MAX":{
                    if(checkExt(result,d)){
                        error += rand*constValue*range*(priority/2);
                    }
                } break;
                case "INC":{
                    if(checkSlopeDir(result,d)){
                        error += rand*constValue*range*priority;
                    }
                } break;
                case "DEC":{
                    if(checkSlopeDir(result,d)){
                        error += rand*constValue*range*priority;
                    }
                } break;
                case "CONST":{
                    if(checkSlopeDir(result,d)){
                        error += rand*constValue*range;
                    }
                } break;
                case "CONV":{
                    if(checkConvexity(result,d)){
                        error += rand*constValue*range*(priority/2);
                    }
                } break;
                case "NCONV":{
                    if(checkConvexity(result,d)){
                        error += rand*constValue*range*(priority/2);
                    }
                } break;
            }
        }
        return error;
    }
    
    public double getFunction(int i, double[] params, TestData data){
        double f = params[0];
	for(int j = 1; j<params.length;j++){
            f += params[j]*data.getX()[j-1][i];
	}
	return f;
    }
    
    public Boolean checkExt(double[] params, TestData data){
        for(double i = data.getMin(); i <= data.getMax(); i += 0.2){
            double res = 0.0;
            for(int p = 1; p < params.length; p++){
                res += params[p]*Math.pow(i, p);
            }
            if(i > data.getMin() && i < data.getMax()){
                if(data.getAction().equals("MIN")){
                    if(res <= data.getValue()){
                        return false;
                    } 
                } else {
                    if(res >= data.getValue()){
                        return false;
                    } 
                }
            }
        }
	return true;
    }
    
    public Boolean checkConvexity(double[] params, TestData data){
            double res = 0.0;
            double res2 = 0.0;
            for(int p = 1; p < params.length; p++){
                res += params[p]*Math.pow(data.getMin(), p);
                res2 += params[p]*Math.pow(data.getMax(), p);
            }
            double res3 = 0.0;
            double g = Math.random();
            for(int p = 1; p < params.length; p++){
                res3 += params[p]*Math.pow(g*data.getMin()+(1-g)*data.getMax(), p);
            }
            if(data.getAction().equals("CONV")){
                if(res3 > g*res+(1-g)*res2){
                    return false;
                }
            } else { 
                if(res3 <= g*res+(1-g)*res2){
                    return false;
                }
            }
	return true;
    }
    
    public Boolean checkSlopeDir(double[] params, TestData data){
        double prevRes = 0.0;
        for(int p = 1; p < params.length; p++){
            prevRes += params[p]*Math.pow(data.getMin(), p);
        }
            
        for(double i = data.getMin()+0.2; i <= data.getMax(); i += 0.2){
            double res = 0.0;
            for(int p = 1; p < params.length; p++){
                res += params[p]*Math.pow(i, p);
            }
            if(i > data.getMin() && i < data.getMax()){
                if(data.getAction().equals("INC")){
                    if(res < prevRes){
                        return false;
                    } 
                } else if(data.getAction().equals("INC")){
                    if(res > prevRes){
                        return false;
                    } 
                } else {
                    if(res != prevRes){
                        return false;
                    }
                }
            }
            prevRes = res;
        }
	return true;
    }
    
    /**
     * This method counts the function, that is being inspected.
     * @param x
     * @return 
     */
    public double func(double x){
        double result;
//        result = 4*x*x*x*x - 5*x*x - 2*x + 4;
//        result = (0.5*x)*(0.5*x)*(0.5*x)*(0.5*x)+4*(0.5*x)*(0.5*x)*(0.5*x)-5*x;
       
        //For test files:
//        result = x*x*x*x - 3*x*x*x - 16*x*x + 2*x - 6;
//        result = 20*Math.sin(x) + 3*x*x - 5*x;
        result = -3*x*x*x - 16*x*x + 2*x - 6;
        
        return result;
    }
    
    /**
     * This method updates the tempreture value.
     * 
     * @param i 
     */
    private void countNewTemp(int i){
        if(i % 500 == 0){
            startingTemp = startingTemp*coolingRate;
        } 
    }
    
    /**
     * This method is used to find the next inspected point.
     * @param previousIndex
     * @return 
     */
    private double findClosePoint(double previousIndex, double min, double max){
        double rand = Math.random();
        if(Math.round(Math.random()*10) % 2 == 0){
            rand = - rand;
        }
        double result = previousIndex + (rand / 10.0);
        while(result < min 
                || result > max){
            rand = Math.random();
            if(Math.round(Math.random()*10) % 2 == 0){
                rand = - rand;
            }
            result = previousIndex + (rand / 10.0);
        }
        //It is divided by 100 to increase accuracy of the result. 
        return result;
    }
    
    private double findClosePoint(double previousIndex, int step){
        double rand = Math.random()*2 - 1;
        double result = previousIndex + (rand / (double)step);
        
        return result;
    }

    public double getStartingTemp() {
        return startingTemp;
    }
        
    
}
